package org.jeecg.modules.putil.wx;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xkcoding.http.HttpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.Strings;
import org.jeecg.common.util.http.HttpClientUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.oss.OssBootUtil;
import org.jeecg.modules.putil.ByteArrayMultipartFile;
import org.jeecg.modules.putil.wx.vo.WxAutoLoginVO;
import org.jeecg.modules.putil.wx.vo.WxNotifyVO;
import org.jeecg.modules.putil.wx.vo.WxUserEncryptionVO;
import org.jeecg.modules.putil.wx.vo.WxUserInfoVO;
import org.jeecg.modules.system.entity.SysUser;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Date;

import static org.jeecg.common.util.ConsoleColors.RED;
import static org.jeecg.common.util.ConsoleColors.RESET;


/**
 * 微信服务端API工具类
 *
 * @author Planifolia.Van
 * @date 2021-01-24
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class WxUtil {

    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
    private static final String CODE_UNLIMITED = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=YOUR_TK";

    private final RedisUtil redisUtil;

    /**
     * 小程序唯一凭证，即 AppID
     */
    @Value("${wx.miniAppId}")
    private String appId;
    /**
     * 小程序唯一凭证密钥，即 AppSecret
     */
    @Value("${wx.miniAppSecret}")
    private String secret;

    /**
     * 获取openId的url
     */
    @Value("${wx.openIdUrl}")
    private String srcOpenIdUrl;

    /**
     * 真实的获取微信OpenId的Url
     */
    private String openIdUrl;


    /**
     * 初始化获取openId的url
     */
    @PostConstruct
    private void initOpenUrl() {
        openIdUrl = srcOpenIdUrl.replace("APPID", appId).replace("SECRET", secret);
    }

    /**
     * 获取小程序全局唯一后台接口调用凭据（access_token）
     *
     * @return AccessToken
     * @see <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html">微信官方文档 auth.getAccessToken</a>
     */
    public String getAccessToken() {
        /* 先判断redis中有没有，没有就去请求 */
        Object accessTokenRedisObject = redisUtil.get(CommonConstant.WECHAT_APPLET_ACCESS_TOKEN);
        if (accessTokenRedisObject != null) {
            return accessTokenRedisObject.toString();
        }
        JSONObject resultJsonObject;
        try {
            resultJsonObject = JSON.parseObject(HttpClientUtil.doOkHttpGet(String.format("%s?grant_type=client_credential&appid=%s&secret=%s",
                    ACCESS_TOKEN_URL, appId, secret), null, null));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        log.info(resultJsonObject.toJSONString());

        /* 判断是否请求成功，根据错误码并返回相应错误信息 */
        if (resultJsonObject.containsKey("errcode")) {
            Integer errCode = resultJsonObject.getInteger("errcode");
            String errorMessage;
            if (errCode == -1) {
                errorMessage = "系统繁忙，此时请稍候再试";
            } else if (errCode == 40001) {
                errorMessage = "AppSecret 错误或者 AppSecret 不属于这个小程序，请开发者确认 AppSecret 的正确性";
            } else if (errCode == 40002) {
                errorMessage = "请确保 grant_type 字段值为 client_credential";
            } else if (errCode == 40013) {
                errorMessage = "不合法的 AppID，请开发者检查 AppID 的正确性，避免异常字符，注意大小写";
            } else {
                errorMessage = resultJsonObject.getString("errmsg");
            }
            throw new JeecgBootException(errorMessage);
        }

        /* 请求成功，存入 redis 并返回 */
        // access_token 获取到的凭证
        String accessToken = resultJsonObject.getString("access_token");
        log.info(accessToken);
        // expires_in 凭证有效时间，单位：秒（目前是7200秒之内的值）
        redisUtil.set(CommonConstant.WECHAT_APPLET_ACCESS_TOKEN, accessToken, resultJsonObject.getInteger("expires_in"));
        return accessToken;
    }


    /**
     * 生成小程序二维码
     *
     * @param scene 携带的数据
     * @param page  跳转的目标路径
     * @return 生成的二维码内容
     */
    public String getMinAppQcCode(String scene, String page) {
        // 制作参数与url
        String wxAcToken = getAccessToken();
        String realRequestURL = CODE_UNLIMITED.replace("YOUR_TK", wxAcToken);
        JSONObject body = new JSONObject();
        body.put("scene", scene);
        body.put("page", page);
        body.put("check_path", false);
        // 调试使用体验版
        body.put("env_version", "trial");
        // 构建OKHTTP3请求
        OkHttpClient client = new OkHttpClient().newBuilder().build();
        MediaType mediaType = MediaType.parse("application/json");
        RequestBody requestBody = RequestBody.create(mediaType, JSONObject.toJSONString(body));
        Request request = new Request.Builder().url(realRequestURL).method("POST", requestBody).build();
        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                assert response.body() != null;
                byte[] bytes = response.body().bytes();
                return java.util.Base64.getEncoder().encodeToString(bytes);
            } else {
                throw new JeecgBootException("获取小程序二维码失败！");
            }
        } catch (IOException e) {
            throw new JeecgBootException("获取小程序二维码失败！");
        }
    }

    /**
     * 生成小程序二维码
     *
     * @param scene 携带的数据
     * @param page  跳转的目标路径
     * @return 生成的二维码内容
     */
    public String getMinAppQcCodeAndUpload(String scene, String page) {
        // 制作参数与url
        String wxAcToken = getAccessToken();
        String realRequestURL = CODE_UNLIMITED.replace("YOUR_TK", wxAcToken);
        JSONObject body = new JSONObject();
        body.put("scene", scene);
        body.put("page", page);
        body.put("check_path", false);
        // 调试使用体验版
        body.put("env_version", "trial");
        // 构建OKHTTP3请求
        OkHttpClient client = new OkHttpClient().newBuilder().build();
        MediaType mediaType = MediaType.parse("application/json");
        RequestBody requestBody = RequestBody.create(mediaType, JSONObject.toJSONString(body));
        Request request = new Request.Builder().url(realRequestURL).method("POST", requestBody).build();
        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                assert response.body() != null;
                byte[] bytes = response.body().bytes();
                return OssBootUtil.upload(new ByteArrayInputStream(bytes), CommonConstant.USER_REC_QC_URL + scene + ".png");
            } else {
                throw new JeecgBootException("获取小程序二维码失败！");
            }
        } catch (IOException e) {
            throw new JeecgBootException("获取小程序二维码失败！");
        }
    }


    public MultipartFile byteToMultipartFile(byte[] bytes, String fileName, String contentType) {
        return new ByteArrayMultipartFile(bytes, fileName, contentType);
    }

    /**
     * 发送微信公众号通知
     *
     * @param wxNotifyVO 发送通知必要的参数信息
     * @return 发送结果
     */
    public String sendWxNotify(WxNotifyVO wxNotifyVO) {
        String url = WxAbilityEnum.SEND_WX_NOTIFY.getUrl(getAccessToken());
        JSONObject json = (JSONObject) JSON.toJSON(wxNotifyVO);
        String result = HttpClientUtil.doOkHttpPost(url, json, null);
        log.info("微信公众号消息发送成功！返回结果:{}", result);
        return result;
    }


    /**
     * 获取用户的OpenId
     *
     * @param code 前端传递过来的code
     * @return 获取结果
     */
    public WxAutoLoginVO getUserOpenId(String code) {
        if (Strings.isEmpty(code)) {
            throw new JeecgBootException("Code不得为空！");
        }
        String res = HttpUtil.get(openIdUrl.replace("JSCODE", code));
        JSONObject jsonObject = JSON.parseObject(res);
        String openid = jsonObject.getString("openid");
        String unionid = jsonObject.getString("unionid");
        String session_key = jsonObject.getString("session_key");
        log.info("微信API获取OpenId结果：{}", jsonObject);
        if (StrUtil.isEmpty(openid)) {
            throw new JeecgBootException("授权失败");
        }
        WxAutoLoginVO wxAutoLoginVO = new WxAutoLoginVO();
        wxAutoLoginVO.setOpenid(openid);
        wxAutoLoginVO.setUnionid(unionid);
        wxAutoLoginVO.setSessionKey(session_key);
        return wxAutoLoginVO;
    }


    /**
     * 获取微信用户信息
     *
     * @param wxUserEncryptionVO 微信用户信息密文
     * @return 解密后的微信用户信息
     */
    private WxUserInfoVO getWxUserInfo(WxUserEncryptionVO wxUserEncryptionVO) {
        String decrypt;
        decrypt = WxUtil.decrypt(wxUserEncryptionVO.getData(), wxUserEncryptionVO.getSessionKey(), wxUserEncryptionVO.getIv());
        if (decrypt == null) {
            throw new JeecgBootException("微信解密失败！");
        }
        WxUserInfoVO wxUserInfoVO = JSONObject.parseObject(decrypt, WxUserInfoVO.class);
        if (wxUserInfoVO == null) {
            throw new JeecgBootException("获取用户信息失败");
        }
        return wxUserInfoVO;
    }

    /**
     * 根据微信用户信息构建成系统用户信息
     *
     * @param wxUserEncryptionVO 微信加密的用户信息
     * @return 构建成的系统用户信息
     */
    public SysUser wxUserInfoToSysUser(WxUserEncryptionVO wxUserEncryptionVO) {
        WxUserInfoVO wxUserInfoVO = getWxUserInfo(wxUserEncryptionVO);
        log.info(RED + "开始构建用户的基本信息" + RESET);
        String username = wxUserInfoVO.getPhoneNumber();
        String realName = org.jeecg.common.util.Strings.maskPhoneNumber(wxUserInfoVO.getPhoneNumber());
        String password = username.substring(username.length() - 6);
        log.info(Strings.toColor("mixPassword : {}", RED), password);
        String phone = wxUserInfoVO.getPhoneNumber();
        String salt = oConvertUtils.randomGen(8);
        String passwordEncode = PasswordUtil.encrypt(username, password, salt);
        SysUser user = new SysUser();
        user.setCreateTime(new Date());
        user.setSalt(salt);
        user.setUsername(username);
        user.setRealname(realName);
        user.setPassword(passwordEncode);
        user.setPhone(phone);
        user.setOpenId(wxUserEncryptionVO.getOpenid());
        user.setStatus(CommonConstant.USER_UNFREEZE);
        user.setDelFlag(CommonConstant.DEL_FLAG_0);
        user.setActivitiSync(CommonConstant.ACT_SYNC_1);
        return user;
    }


    /**
     * 微信解密
     *
     * @param data 加密的用户数据
     * @param key  微信sessionKey
     * @param iv
     * @return
     */
    public static String decrypt(String data, String key, String iv) {
        //被加密的数据
        byte[] dataByte = Base64.decodeBase64(data);
        //加密秘钥
        byte[] keyByte = Base64.decodeBase64(key);
        //偏移量
        byte[] ivByte = Base64.decodeBase64(iv);
        try {
            AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivByte);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(keyByte, "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            return new String(cipher.doFinal(dataByte), StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
                 InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            log.error("微信解密失败！\n堆栈异常消息:{}", e.getMessage());
        }
        return null;
    }
}
